阻塞IO和非阻塞IO

常见的IO操作有read和write,通常IO操作都是阻塞I/O,即当前调用read时如果没有数据收到,那么线程或者进行就会被挂起,直到收到数据。

对于非阻塞I/O,通过fcntl(POSIX)或ioctl(UNIX)设为非阻塞模式,这时当你调用read时,如果有数据收到,就返回数据,如果没有数据收到,就立刻返回。

I/O多路复用

多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)。

I/O多路复用又被称为事件驱动,操作系统提供了一个功能,当某个socket可读或者可写的时候,可以给你一个通知,这样当配合非阻塞的socket使用时,只有当系统通知我哪个描述符可读了,我才去执行read操作,可以保证每次read都能读到有效数据而不做纯返回-1和EAGAIN的无用功。

Poll介绍

poll的机制与select类似,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。poll和select的共同缺点就是包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而 不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。poll的函数原型如下:

1
2
#include <poll.h>
int poll(struct pollfd* fds, unsigned int fds, int timeout);

其中,pollfd结构体的定义如下:

1
2
3
4
5
struct pollfd {
int fd; // 文件描述符
short events; // 等待的事件
short revents; // 实际发生了的事件
};

每一个pollfd结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示poll()监视多个文件描述符,每个结构体的events域是监视该文件描述符的事件掩码,由用户设置。revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域,常见的事件如下:

1
2
3
4
5
6
7
8
POLLIN 有数据可读
POLLRDNORM 有普通数据可读
POLLRDBAND 有优先数据可读
POLLPRI 有紧迫数据可读
POLLOUT 写数据不会导致阻塞
POLLWRNORM      写普通数据不会导致阻塞
POLLWRBAND      写优先数据不会导致阻塞
POLLMSGSIGPOLL      消息可用

POLLIN | POLLPRI等价于select()的读事件,POLLOUT | POLLWRBAND等价于select()的写事件。如果要监视一个文件描述符是否可读和可写,我们可以设置events为POLLIN | POLLOUT。在poll返回时,我们可以检查revents中的标识,对应于文件描述符请求的events结构体。如果POLLIN事件被设置,则该文件描述符可以被读取而不阻塞;如果POLLOUT被设置,则文件描述符可以写入而不导致阻塞。

timeout参数指定等待的毫秒数,无论I/O是否准备好,poll都会返回。timeout指定为负数则表示无限超时,使poll()一直挂起直到一个指定事件发生;timeout为0则表示poll调用立即返回并列出准备好的I/O文件描述符,但并不等待其他的事件。

poll调用成功时,返回结构体的revents域不为0的文件描述符的个数;如果在超时前没有任何事件发生,poll()返回0;失败时返回-1。

Poll举例

编写一个echo server程序,功能是客户端向服务器发送信息,服务器接收输出并原样返回给客户端,客户端接收到输出到终端。

服务端代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <poll.h>
#include <unistd.h>
#include <sys/types.h>
#define IPADDRESS "127.0.0.1"
#define PORT 8787
#define MAXLINE 1024
#define LISTENQ 5
#define OPEN_MAX 1000
#define INFTIM -1
static int socket_bind(const char* ip, int port);
static void do_poll(int listenfd);
static void handle_connection(struct pollfd* connfds, int num);
int main(int argc, char** argv) {
int listenfd, connfd, sockfd;
struct sockaddr_in cliaddr;
socklen_t cliaddrlen;
listenfd = socket_bind(IPADDRESS, PORT);
listen(listenfd, LISTENQ);
do_poll(listenfd);
return 0;
}
static int socket_bind(const char* ip, int port) {
int listenfd;
struct sockaddr_in servaddr;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1) {
perror("socket error:");
exit(1);
}
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, ip, &servaddr.sin_addr);
servaddr.sin_port = htons(port);
if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) {
perror("bind error: ");
exit(1);
}
return listenfd;
}
static void do_poll(int listenfd) {
int connfd, sockfd;
struct sockaddr_in cliaddr;
socklen_t cliaddrlen;
struct pollfd clientfds[OPEN_MAX];
int maxi;
int i;
int nready;
clientfds[0].fd = listenfd;
clientfds[0].events = POLLIN;
for (i = 1; i < OPEN_MAX; i++) {
clientfds[i].fd = -1;
}
maxi = 0;
for (; ;) {
nready = poll(clientfds, maxi + 1, INFTIM);
if (nready == -1) {
perror("poll error: ");
exit(1);
}
if (clientfds[0].revents & POLLIN) {
cliaddrlen = sizeof(cliaddr);
if ((connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &cliaddrlen)) == -1) {
if (errno == EINTR) {
continue;
} else {
perror("accept error: ");
exit(1);
}
}
fprintf(stdout, "accept a new client: %s:%d\n", inet_ntoa(cliaddr.sin_addr), cliaddr.sin_port);
for (i = 1; i < OPEN_MAX; i++) {
if (clientfds[i].fd < 0) {
clientfds[i].fd = connfd;
break;
}
}
if (i == OPEN_MAX) {
fprintf(stderr, "too many clients.\n");
exit(1);
}
clientfds[i].events = POLLIN;
maxi = (i > maxi ? i : maxi);
if (--nready <= 0) {
continue;
}
}
handle_connection(clientfds, maxi);
}
}
static void handle_connection(struct pollfd* connfds, int num) {
int i, n;
char buf[MAXLINE];
memset(buf, 0, MAXLINE);
for (i = 1; i <= num; i++) {
if (connfds[i].fd < 0) {
continue;
}
if (connfds[i].revents & POLLIN) {
n = read(connfds[i], fd, buf, MAXLINE);
if (n == 0) {
close(connfds[i].fd);
connfds[i].fd = -1;
continue;
}
write(STDOUT_FILENO, buf, n);
write(connfds[i], fd, buf, n);
}
}
}

客户端代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <netinet/in.h>
#include <sys/socket.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <poll.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#define MAXLINE 1024
#define IPADDRESS "127.0.0.1"
#define SERV_PORT 8787
#define max(a, b) (a > b) ? a : b
static void handle_connection(int sockfd);
int main(int argc, char* argv[]) {
int sockfd;
struct sockaddr_in servaddr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, IPADDRESS, &servaddr.sin_addr);
connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
handle_connection(sockfd);
return 0;
}
static void handle_connection(int sockfd) {
char sendline[MAXLINE], recvline[MAXLINE];
int maxfdp, stdineof;
struct pollfd pfds[2];
pfds[0].fd = sockfd;
pfds[0].events = POLLIN;
pfds[1].fd = STDIN_FILENO;
pfds[1].events = POLLIN;
for (; ;) {
poll(pfds, 2, -1);
if (pfds[0].revents & POLLIN) {
n = read(sockfd, recvline, MAXLINE);
if (n == 0) {
fprintf(stderr,"client: server is closed.\n");
close(sockfd);
}
write(STDOUT_FILENO, recvline, n);
}
//测试标准输入是否准备好
if (pfds[1].revents & POLLIN) {
n = read(STDIN_FILENO, sendline, MAXLINE);
if (n == 0) {
shutdown(sockfd, SHUT_WR);
continue;
}
write(sockfd, sendline, n);
}
}
}